package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.temporal.TemporalUnit;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;

@Component
public class CacheClient {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);


    /**
     * 写入缓存
     * @param key
     * @param value
     * @param expire
     * @param timeUnit
     */
    public void set(String key, Object value, Long expire, TimeUnit timeUnit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), expire, timeUnit);
    }

    /**
     * 对象写入缓存，设置逻辑过期时间
     * @param key
     * @param value
     * @param expire
     * @param timeUnit
     */
    public void setWithLogicalExpire(String key, Object value, Long expire, TimeUnit timeUnit) {
        RedisData redisData = new RedisData();
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(expire)));
        redisData.setData(value);
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 指定key查询（缓存空值解决缓存穿透）
     * @param id
     * @return
     */
    public <R, T> R queryWithCacheNull(String prefix, T id, Class<R> type, Function<T, R> fallBack,
                                       Long expire, TimeUnit timeUnit) {
        String key = prefix + id;
        // 查询redis缓存是否存在对应数据
        String resultJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(resultJson)) {
            // 存在，直接返回
            R result = JSONUtil.toBean(resultJson, type);
            return result;
        }
        if ("".equals(resultJson)) {
            // 查询到空缓存，返回错误信息
            return null;
        }
        // 不存在，查询数据库是否有对应数据
        R result = fallBack.apply(id);
        if (result == null) {
            // 不存在，将空值写入缓存，并返回错误信息
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        // 存在，将数据库数据写入缓存
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(result), expire, timeUnit);


        // 返回
        return result;
    }

    /**
     * 查询商铺（逻辑过期解决缓存击穿问题）
     * @param id
     * @return
     */
    public <R, T> R queryWithLogicalExpire(String prefix, T id, Class<R> type, Function<T, R> dbFallback,
                                            Long expire, TimeUnit timeUnit) {
        String key = prefix + id;
        // 查询redis缓存是否存在对应数据
        String resultJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(resultJson)) {
            // 不存在，直接返回
            return null;
        }
        // 反序列化json为R类型
        RedisData redisData = JSONUtil.toBean(resultJson, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        R result = JSONUtil.toBean(data, type);
        // 缓存中存在对应数据，判断是否过期
        LocalDateTime expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 未过期，则返回
            return result;
        }

        // 过期，尝试加锁
        if (tryLock(id)) {
            // 加锁成功，开启一个线程进行缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 查询数据库
                    R res = dbFallback.apply(id);
                    // 设置缓存
                    setWithLogicalExpire(key, res, expire, timeUnit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    unLock(id);
                }
            });
        }

        // 返回
        return result;
    }

    /**
     * 尝试获取分布式锁
     * @param id
     * @return
     */
    private <T> boolean tryLock(T id) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_SHOP_KEY + id, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     * @param id
     */
    private <T> void unLock(T id) {
        stringRedisTemplate.delete(LOCK_SHOP_KEY + id);
    }

}
